﻿using System;
using System.Reflection;
using System.Collections.Generic;

namespace Narwhunderful.Aphelia.Framework.IO.Serialization
{

    public sealed class ObjectFactory
    {
        private readonly object[] _emptyObjectArray = new object[0];

        public object CreateObject(Property property)
        {
            if (property == null) throw new ArgumentNullException("property");

            // Is it NullProperty?
            var nullProperty = property as NullProperty;
            if (nullProperty != null)
            {
                return null;
            }

            // Is it ComplexReferenceProperty?
            var complexReferenceProperty = property as ComplexReferenceProperty;
            if (complexReferenceProperty != null)
            {
                return CreateObjectFromComplexReferenceProperty(complexReferenceProperty);
            }

            if (property.Type == null)
            {
                // there is no property type and no expected type defined. Give up!
                throw new InvalidOperationException(string.Format("Property type is not defined. Property: \"{0}\"",
                                                                  property.Name));
            }

            // Is it SimpleProperty?
            var simpleProperty = property as SimpleProperty;
            if (simpleProperty != null)
            {
                return CreateObjectFromSimpleProperty(simpleProperty);
            }

            // Is it multidimensional array?
            var multiDimensionalArrayProperty = property as MultiDimensionalArrayProperty;
            if (multiDimensionalArrayProperty != null)
            {
                return CeateObjectFromMultidimensionalArrayProperty(multiDimensionalArrayProperty);
            }

            // Is it singledimensional array?
            var singleDimensionalArrayProperty = property as SingleDimensionalArrayProperty;
            if (singleDimensionalArrayProperty != null)
            {
                return CreateObjectFromSingleDimensionalArrayProperty(singleDimensionalArrayProperty);
            }

            // Is it dictionary?
            var dictionaryProperty = property as DictionaryProperty;
            if (dictionaryProperty != null)
            {
                return CreateObjectFromDictionaryProperty(dictionaryProperty);
            }

            // Is it collection?
            var collectionProperty = property as CollectionProperty;
            if (collectionProperty != null)
            {
                return CreateObjectFromCollectionProperty(collectionProperty);
            }

            // Is it complex type? Class? Structure?
            var complexProperty = property as ComplexProperty;
            if (complexProperty != null)
            {
                return CreateObjectFromComplexProperty(complexProperty);
            }

            // No idea what it is
            throw new InvalidOperationException(string.Format("Unknown Property type: {0}", property.GetType().Name));
        }

        private static object CreateObjectFromSimpleProperty(SimpleProperty property)
        {
            return property.Value;
        }

        private object CreateObjectFromComplexProperty(ComplexProperty property)
        {
            object obj = Helpers.CreateInstance(property.Type);
            property.Value = obj; // remember if there is a reference to this item to be resolved

            FillProperties(obj, property.Properties);

            return obj;
        }

        private object CreateObjectFromComplexReferenceProperty(ComplexReferenceProperty property)
        {
            object obj = property.ReferenceTarget.Value;
            System.Diagnostics.Debug.Assert(obj != null, "ReferenceTarget.Value should already have been created");
            return obj;
        }

        private object CreateObjectFromCollectionProperty(CollectionProperty property)
        {
            Type type = property.Type;
            object collection = Helpers.CreateInstance(type);

            // fill the properties
            FillProperties(collection, property.Properties);

            // Fill the items but only if the "Add" method was found, which has only 1 parameter
            MethodInfo methodInfo = collection.GetType().GetMethod("Add");
            if (methodInfo != null)
            {
                ParameterInfo[] parameters = methodInfo.GetParameters();
                if (parameters.Length == 1)
                {
                    foreach (Property item in property.Items)
                    {
                        object value = CreateObject(item);
                        methodInfo.Invoke(collection, new[] { value });
                    }
                }
            }
            return collection;
        }

        private object CreateObjectFromDictionaryProperty(DictionaryProperty property)
        {
            object dictionary = Helpers.CreateInstance(property.Type);

            // fill the properties
            FillProperties(dictionary, property.Properties);

            // fill items, but only if Add(key, value) was found
            MethodInfo methodInfo = dictionary.GetType().GetMethod("Add");
            if (methodInfo != null)
            {
                ParameterInfo[] parameters = methodInfo.GetParameters();
                if (parameters.Length == 2)
                {
                    foreach (KeyValueItem item in property.Items)
                    {
                        object keyValue = CreateObject(item.Key);
                        object valueValue = CreateObject(item.Value);

                        methodInfo.Invoke(dictionary, new[] { keyValue, valueValue });
                    }
                }
            }

            return dictionary;
        }

        private void FillProperties(object obj, IEnumerable<Property> properties)
        {
            foreach (Property property in properties)
            {
                PropertyInfo propertyInfo = obj.GetType().GetProperty(property.Name);
                if (propertyInfo == null) continue;

                object value = CreateObject(property);
                if (value == null) continue;

                propertyInfo.SetValue(obj, value, _emptyObjectArray);
            }
        }

        private object CreateObjectFromSingleDimensionalArrayProperty(SingleDimensionalArrayProperty property)
        {
            int itemsCount = property.Items.Count;

            Array array = CreateArrayInstance(property.ElementType, new[] { itemsCount }, new[] { property.LowerBound });

            // Items
            for (int index = property.LowerBound; index < property.LowerBound + itemsCount; index++)
            {
                Property item = property.Items[index];
                object value = CreateObject(item);
                if (value != null)
                {
                    array.SetValue(value, index);
                }
            }

            return array;
        }

        private object CeateObjectFromMultidimensionalArrayProperty(MultiDimensionalArrayProperty property)
        {
            // determine array type
            MultiDimensionalArrayCreatingInfo creatingInfo =
                GetMultiDimensionalArrayCreatingInfo(property.DimensionInfos);

            // Instantiating the array
            Array array = CreateArrayInstance(property.ElementType, creatingInfo.Lengths, creatingInfo.LowerBounds);

            // fill the values
            foreach (MultiDimensionalArrayItem item in property.Items)
            {
                object value = CreateObject(item.Value);
                if (value != null)
                {
                    array.SetValue(value, item.Indexes);
                }
            }

            return array;
        }

        private static Array CreateArrayInstance(Type elementType, int[] lengths, int[] lowerBounds)
        {
#if (SMARTPHONE || SILVERLIGHT || XBOX)
            return Array.CreateInstance(elementType, lengths);
#else
            return Array.CreateInstance(elementType, lengths, lowerBounds);
#endif
        }

        private static MultiDimensionalArrayCreatingInfo GetMultiDimensionalArrayCreatingInfo(IEnumerable<DimensionInfo> infos)
        {
            var lengths = new List<int>();
            var lowerBounds = new List<int>();
            foreach (DimensionInfo info in infos)
            {
                lengths.Add(info.Length);
                lowerBounds.Add(info.LowerBound);
            }

            var result = new MultiDimensionalArrayCreatingInfo();
            result.Lengths = lengths.ToArray();
            result.LowerBounds = lowerBounds.ToArray();
            return result;
        }

        private class MultiDimensionalArrayCreatingInfo
        {
            public int[] Lengths { get; set; }
            public int[] LowerBounds { get; set; }
        }

    }
}
